package org.xx.armory.commons;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.DateSerializer;
import com.fasterxml.jackson.databind.type.MapType;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;

import static java.util.Arrays.asList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.unmodifiableList;
import static org.apache.commons.lang3.StringUtils.substring;
import static org.apache.commons.lang3.math.NumberUtils.toInt;

/**
 * JSON相关的工具类。
 */
public final class JsonUtils {
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper()
            .enable(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
            .enable(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .disable(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES)
            .registerModule(new SimpleModule()
                                    .addSerializer(Date.class, DateSerializer.instance.withFormat(true, null))
                                    .addSerializer(LocalDate.class, LocalDateTimeSerializer.dateWithFormat(DateTimeFormatter.ISO_LOCAL_DATE))
                                    .addSerializer(LocalTime.class, LocalDateTimeSerializer.timeWithFormat(DateTimeFormatter.ISO_LOCAL_TIME))
                                    .addSerializer(LocalDateTime.class, LocalDateTimeSerializer.dateTimeWithFormat(DateTimeFormatter.ISO_LOCAL_DATE_TIME))
                                    .addSerializer(IntegerEnum.class, new IntegerEnumJsonSerializer())
                                    .addSerializer(StringEnum.class, new StringEnumJsonSerializer())
                                    .addSerializer(BigDecimal.class, new BigDecimalJsonSerializer())
            );

    private static final MapType MAP_TYPE = JSON_MAPPER.getTypeFactory().constructMapType(HashMap.class, String.class,
                                                                                          Object.class);

    private JsonUtils() {
        throw new AssertionError();
    }

    /**
     * 将JSON字符串解析为Map。
     *
     * @param s
     *         待解析的字符串。
     * @return 解析结果。如果参数 {@code s} 是 {@code null} 或者只包含空白字符或者值是 {@literal "null"}则返回空 Map。
     * @throws java.io.UncheckedIOException
     *         如果解析字符串出现错误。
     */
    public static Map<String, Object> parseJsonObject(
            String s
    ) {
        if (s == null) {
            return emptyMap();
        }

        s = s.strip();

        if (s.isEmpty() || "null".equals(s)) {
            return emptyMap();
        }

        try {
            return JSON_MAPPER.readValue(s, MAP_TYPE);
        } catch (IOException ex) {
            throw new UncheckedIOException(ex);
        }
    }

    /**
     * 将Map格式化为JSON字符串。
     *
     * @param obj
     *         待格式化的Map对象。
     * @return 格式化结果。
     * @throws java.io.UncheckedIOException
     *         如果格式化过程出现错误。
     */
    public static String formatJsonObject(
            Object obj
    ) {
        if (obj == null) {
            return "{ }";
        } else {
            try {
                return JSON_MAPPER.writeValueAsString(obj);
            } catch (IOException ex) {
                throw new UncheckedIOException(ex);
            }
        }
    }

    public static String readDeepStringProperty(
            Object object,
            String fullName
    ) {
        final var value = readDeepProperty(object, fullName);
        return value != null ? value.toString() : "";
    }

    public static int readDeepIntProperty(
            Object object,
            String fullName
    ) {
        final var value = readDeepProperty(object, fullName);
        if (value instanceof Integer) {
            return (Integer) value;
        } else if (value instanceof Number) {
            return ((Number) value).intValue();
        } else if (value instanceof String) {
            try {
                return Integer.parseInt((String) value);
            } catch (NumberFormatException ignored) {
                throw new JsonPropertyException(object, fullName, "int");
            }
        } else {
            throw new JsonPropertyException(object, fullName, "int");
        }
    }

    public static long readDeepLongProperty(
            Object object,
            String fullName
    ) {
        final var value = readDeepProperty(object, fullName);
        if (value instanceof Long) {
            return (Long) value;
        } else if (value instanceof Number) {
            return ((Number) value).longValue();
        } else if (value instanceof String) {
            try {
                return Long.parseLong((String) value);
            } catch (NumberFormatException ignored) {
                throw new JsonPropertyException(object, fullName, "long");
            }
        } else {
            throw new JsonPropertyException(object, fullName, "long");
        }
    }

    public static double readDeepDoubleProperty(
            Object object,
            String fullName
    ) {
        final var value = readDeepProperty(object, fullName);
        if (value instanceof Double) {
            return (Double) value;
        } else if (value instanceof Number) {
            return ((Number) value).doubleValue();
        } else if (value instanceof String) {
            try {
                return Double.parseDouble((String) value);
            } catch (NumberFormatException ignored) {
                throw new JsonPropertyException(object, fullName, "double");
            }
        } else {
            throw new JsonPropertyException(object, fullName, "double");
        }
    }

    public static boolean readDeepBoolProperty(
            Object object,
            String fullName
    ) {
        final var value = readDeepProperty(object, fullName);
        if (value instanceof Boolean) {
            return (Boolean) value;
        } else if (value instanceof String) {
            return ((String) value).equalsIgnoreCase("true");
        } else {
            throw new JsonPropertyException(object, fullName, "boolean");
        }
    }

    public static BigDecimal readDeepDecimalProperty(
            Object object,
            String fullName
    ) {
        final var value = readDeepProperty(object, fullName);
        if (value instanceof BigDecimal) {
            return (BigDecimal) value;
        } else if (value instanceof BigInteger) {
            return new BigDecimal((BigInteger) value);
        } else if (value instanceof Number) {
            return BigDecimal.valueOf(((Number) value).doubleValue());
        } else if (value instanceof String) {
            try {
                return new BigDecimal((String) value);
            } catch (NumberFormatException ignored) {
                throw new JsonPropertyException(object, fullName, "BigDecimal");
            }
        } else {
            throw new JsonPropertyException(object, fullName, "BigDecimal");
        }
    }

    public static <E extends Enum<E> & IntegerEnum> E readDeepIntEnumProperty(
            Class<E> clazz,
            Object object,
            String fullName
    ) {
        final var value = readDeepProperty(object, fullName);
        if (clazz.isInstance(value)) {
            return clazz.cast(value);
        } else if (value instanceof Number) {
            final int iv = ((Number) value).intValue();
            for (final E element : clazz.getEnumConstants()) {
                if (element.value() == iv) {
                    return element;
                }
            }
            throw new JsonPropertyException(object, fullName, clazz.getTypeName());
        } else if (value instanceof CharSequence) {
            if (((CharSequence) value).length() == 0) {
                return null;
            }

            final var iv = toInt(value.toString(), 0);
            for (final var element : clazz.getEnumConstants()) {
                if (element.value() == iv) {
                    return element;
                }
            }
            throw new JsonPropertyException(object, fullName, clazz.getTypeName());
        } else {
            throw new JsonPropertyException(object, fullName, clazz.getTypeName());
        }
    }

    public static <E extends Enum<E> & StringEnum> E readDeepStringEnumProperty(
            Class<E> clazz,
            Object object,
            String fullName
    ) {
        final var value = readDeepProperty(object, fullName);
        if (clazz.isInstance(value)) {
            return clazz.cast(value);
        } else if (value instanceof CharSequence) {
            final var sv = value.toString();
            for (final var element : clazz.getEnumConstants()) {
                if (Objects.equals(element.stringValue(), sv)) {
                    return element;
                }
            }
            throw new JsonPropertyException(object, fullName, clazz.getTypeName());
        } else {
            throw new JsonPropertyException(object, fullName, clazz.getTypeName());
        }
    }

    public static List<Object> readDeepListProperty(
            Object object,
            String fullName
    ) {
        final var value = readDeepProperty(object, fullName);
        if (value instanceof Collection) {
            return List.copyOf((Collection<?>) value);
        } else if (value instanceof int[]) {
            return Arrays.stream((int[]) value).boxed().collect(Collectors.toList());
        } else if (value instanceof long[]) {
            return Arrays.stream((long[]) value).boxed().collect(Collectors.toList());
        } else if (value instanceof float[]) {
            final List<Double> result = new ArrayList<>(((float[]) value).length);
            for (final float item : (float[]) value) {
                result.add((double) item);
            }
            return unmodifiableList(result);
        } else if (value instanceof boolean[]) {
            final List<Boolean> result = new ArrayList<>(((boolean[]) value).length);
            for (final boolean item : (boolean[]) value) {
                result.add(item);
            }
            return unmodifiableList(result);
        } else if (value instanceof double[]) {
            return Arrays.stream((double[]) value).boxed().collect(Collectors.toList());
        } else if (value instanceof Object[]) {
            return asList((Object[]) value);
        } else {
            throw new JsonPropertyException(object, fullName, "List");
        }
    }

    public static Map<String, Object> readDeepMapProperty(
            Object object,
            String fullName
    ) {
        final var value = readDeepProperty(object, fullName);
        if (value instanceof Map) {
            return ((Map<?, ?>) value).entrySet().stream()
                                      .collect(Collectors.toMap(
                                              entry -> {
                                                  final Object key = entry.getKey();
                                                  return key != null ? key.toString() : "";
                                              },
                                              Map.Entry::getValue
                                      ));
        } else {
            throw new JsonPropertyException(object, fullName, "Map");
        }
    }

    public static Object readDeepProperty(
            Object object,
            String fullName
    )
            throws JsonPropertyException {
        var result = object;
        final var parser = new FullNameParser(fullName);
        while (parser.hasNext() && result != null) {
            final var token = parser.next();
            if (token.charAt(0) == '[' && token.charAt(token.length() - 1) == ']') {
                // array
                final var index = toInt(substring(token, 1, -1), -1);
                if (index < 0) {
                    throw new ArrayIndexOutOfBoundsException("Illegal index: " + token);
                }
                result = readArrayItem(result, index);
            } else {
                // property
                result = readObjectProperty(result, token);
            }
        }

        return result;
    }

    private static Object readArrayItem(
            Object object,
            int index
    )
            throws JsonPropertyException {
        assert object != null;

        if (object instanceof final Collection<?> c) {
            final Iterator<?> iterator = c.iterator();
            int i = 0;
            while (iterator.hasNext()) {
                if (i < index) {
                    iterator.next();
                    ++i;
                } else if (i == index) {
                    return iterator.next();
                }
            }
            throw new JsonPropertyException(object, " [" + index + "]");
        } else if (object instanceof final int[] array) {
            if (index < array.length) {
                return array[index];
            } else {
                throw new JsonPropertyException(object, " [" + index + "]");
            }
        } else if (object instanceof final long[] array) {
            if (index < array.length) {
                return array[index];
            } else {
                throw new JsonPropertyException(object, " [" + index + "]");
            }
        } else if (object instanceof final float[] array) {
            if (index < array.length) {
                return (double) array[index];
            } else {
                throw new JsonPropertyException(object, " [" + index + "]");
            }
        } else if (object instanceof final boolean[] array) {
            if (index < array.length) {
                return array[index];
            } else {
                throw new JsonPropertyException(object, " [" + index + "]");
            }
        } else if (object instanceof final double[] array) {
            if (index < array.length) {
                return array[index];
            } else {
                throw new JsonPropertyException(object, " [" + index + "]");
            }
        } else if (object instanceof final Object[] array) {
            if (index < array.length) {
                return array[index];
            } else {
                throw new JsonPropertyException(object, " [" + index + "]");
            }
        } else {
            throw new JsonPropertyException(object, " [" + index + "]");
        }
    }

    private static Object readObjectProperty(
            Object object,
            String name
    )
            throws JsonPropertyException {
        assert object != null;

        if (object instanceof final Map<?, ?> m) {
            return m.entrySet().stream()
                    .filter(entry -> {
                        final var key = entry.getKey();
                        final var sKey = key != null ? key.toString() : "";
                        return sKey.equals(name);
                    })
                    .map(entry -> {
                        final var value = entry.getValue();
                        return value != null ? value : "";
                    })
                    .findAny()
                    .orElseThrow(() -> new JsonPropertyException(object, " [\"" + name + "\"]"));
        } else {
            throw new JsonPropertyException(object, " [\"" + name + "\"]");
        }
    }

    public static class IntegerEnumJsonSerializer
            extends JsonSerializer<IntegerEnum> {
        @Override
        public void serialize(
                IntegerEnum value,
                JsonGenerator gen,
                SerializerProvider serializers
        )
                throws IOException {
            assert value != null;

            gen.writeNumber(value.value());
        }
    }

    public static class StringEnumJsonSerializer
            extends JsonSerializer<StringEnum> {
        @Override
        public void serialize(
                StringEnum value,
                JsonGenerator gen,
                SerializerProvider serializers
        )
                throws IOException {
            assert value != null;

            gen.writeString(value.stringValue());
        }
    }

    public static class BigDecimalJsonSerializer
            extends JsonSerializer<BigDecimal> {
        @Override
        public void serialize(
                BigDecimal value,
                JsonGenerator gen,
                SerializerProvider serializers
        )
                throws IOException {
            assert value != null;

            value = value.setScale(5, RoundingMode.HALF_EVEN).stripTrailingZeros();
            gen.writeString(value.toPlainString());
        }
    }

    private static class FullNameParser
            extends StringTokenParser {

        public FullNameParser(
                String content
        ) {
            super(content);
        }

        @Override
        public String next() {
            /*
             * 0 - 初始，尚不能确定读取字段或下标。
             * 1 - 已发现左括号，需要解析数字或者右括号
             * 2 - 未发现左括号，需要解析字段名
             * 3 - 找到右括号，解析下标完成
             * 4 - 找到点号或者左括号，解析字段名完成
             * 9 - 状态错误
             */
            int status = 0;
            StringBuilder buffer = new StringBuilder();
            while (status == 0 || status == 1 || status == 2) {
                final int ch = readChar();
                switch (status) {
                    case 0:
                        if (Character.isWhitespace(ch)) {
                            // 忽略空格。
                        } else if (ch == '[') {
                            buffer.appendCodePoint(ch);
                            status = 1;
                        } else if (ch == '.') {
                            status = 2;
                        } else if (Character.isJavaIdentifierStart(ch)) {
                            buffer.appendCodePoint(ch);
                            status = 2;
                        } else {
                            setIllegalState();
                        }
                        break;
                    case 1:
                        if (Character.isWhitespace(ch)) {
                            // 忽略空格。
                        } else if (Character.isDigit(ch)) {
                            buffer.appendCodePoint(ch);
                        } else if (ch == ']') {
                            if (buffer.length() > 1) {
                                buffer.appendCodePoint(ch);
                                status = 3;
                            } else {
                                setIllegalState();
                            }
                        } else {
                            setIllegalState();
                        }
                        break;
                    case 2:
                        if (Character.isWhitespace(ch)) {
                            // 忽略空格。
                        } else if (ch == '.' || ch == -1) {
                            if (buffer.length() > 1) {
                                status = 4;
                            } else {
                                setIllegalState();
                            }
                        } else if (ch == '[') {
                            putBack();
                            if (buffer.length() > 0) {
                                status = 4;
                            } else {
                                setIllegalState();
                            }
                        } else if (Character.isJavaIdentifierPart(ch)) {
                            buffer.appendCodePoint(ch);
                        } else {
                            setIllegalState();
                        }
                        break;
                    default:
                        assert false;
                }
            }

            return buffer.toString();
        }
    }
}
